const std = @import("std");
const net = std.net;
const mem = std.mem;

const Address = net.Address;
const Stream = net.Stream;
const StreamServer = net.StreamServer;
const Thread = std.Thread;

const io = @import("io.zig");


const Options = struct { opt: Option, addr: Address };
const Option = enum {
    Connect,
    Host,
    Quit,
};

const PORT:u16 = 9876;

pub fn main() !void {
    const options = try get_options();
    switch (options.opt) {
        Option.Host => {
            var server = net.StreamServer.init(net.StreamServer.Options{});
            defer server.deinit();

            try server.listen(options.addr);

            const recieve_conn = try server.accept();
            defer recieve_conn.stream.close();
            const send_conn = try server.accept();
            defer send_conn.stream.close();

            try io.print("Someone has connected.\n");

            const recieve_thread = try Thread.spawn(Thread.SpawnConfig{}, recieve, .{recieve_conn.stream});
            try send(send_conn.stream);

            recieve_thread.join();
        },
        Option.Connect => {
            var send_stream = try net.tcpConnectToAddress(options.addr);
            defer send_stream.close();
            var recieve_stream =  try net.tcpConnectToAddress(options.addr);
            defer recieve_stream.close();

            try io.print("Connected to the remote host\n");

            const recieve_thread = try Thread.spawn(Thread.SpawnConfig{}, recieve, .{recieve_stream});
            try send(send_stream);

            recieve_thread.join();
        },
        Option.Quit => return,
    }
}

/// Print a menu with commands and apply classify_option on stdin.
/// Reads stdin again if classify_option returned none.
/// Returns an error if fails to read stdin or write to stdout.
fn get_options() !Options {
    _ = try io.print(
        \\-------------------
        \\:connect [address]
        \\:host [address]
        \\:quit or :q
        \\-------------------
        \\
    );

    while (true) {
        var input = try io.input("\n");

        const result = classify_option(input) catch {
            _ = try io.print("Invalid IP address.\n");
            continue;
        };

        if (result != null) return result.? else _ = try io.print("Unknown command.\n");
    }
}

/// Returns an error if fails to parse an address.
/// Returns null if failed to recognise a command.
fn classify_option(input: []u8) !?Options {
    if (input[0] != ':') return null;

    var options = Options{ .opt = undefined, .addr = undefined };

    if (mem.eql(u8, input[1..9], "connect ")) {
        options.opt = Option.Connect;
        options.addr = try Address.parseIp(input[9..], PORT);
    } else if (mem.eql(u8, input[1..6], "host ")) {
        options.opt = Option.Host;
        options.addr = try Address.parseIp(input[6..], PORT);
    } else if (mem.eql(u8, input[1..], "quit") or (input[1] == 'q')) {
        options.opt = Option.Quit;
    } else {
        return null;
    }

    return options;
}

fn send(stream: Stream) !void {
    while (true) {
        const input = try io.input("-> ");

        if (input.len == 0) continue;
        if ((input[0] == ':') and (input[1] == 'q')) break;
        _ = try stream.write(input);
    }
}

fn recieve(stream: Stream) !void {
    var nulls: u2 = 0;
    while (true) {
        var buffer: [256]u8 = undefined;

        const read = try stream.read(&buffer);
        if (read == 0) {
            if (nulls > 2) return;
            nulls += 1;
        } else {
            nulls = 0;
            try io.print(buffer[0..read]);
            try io.print("\n");
        }
    }
}
